<?
    /* -*- Mode: PHP5; tab-width: 4; indent-tabs-mode: nil; basic-offset: 4 -*- */

    /*********************************************************
    *             --== InArch ==--
    *
    * DataTree handler. This class is a wrapper around the SQL
    * Nested Set Tree Hierarchy model implementation originally
    * suggested by Joe Celko. For a more detailed description see 
    * 
    * @link http://developer.essentialmind.com/wiki/INA/archs/internals/solutions/web/datatree
    *
    * @author Kulikov Alexey <a.kulikov@gmail.com>
    * @version n/a
    * @since 20.04.2005
    * @copyright essentialmind.com 2005
    *
    *********************************************************/

    /***
    * Class tree
    ****
    * This class is a command wrapper for the User Defined Functions
    * stored in the PostgreSQL Database for the operation with
    * data based on Joe Celko's Nested Sets theory and some
    * black magic voodoo charm written by A. Kulikov.
    *
    * This class comes with a unitTest which asserts all the methods
    * for correct functionality. Yet as the actual logic is implemented
    * in the data layer, there is little to test :))
    ****
    * @author A Kulikov <a.kulikov@gmail.com>
    * @version n/a
    * @since 02.05.2005
    * @copyright essentialmind.com 2005
    ***/
    class tree{
        /**
         * reference to the database object
         *
         * @var ADOConnection
         */
        private $db;
        
        /**
         * name of the table where the sctructure resides
         *
         * @var string
         */
        private $table;
        
        /**
         * reference to the log object
         *
         * @var unknown
         */
        private $errorRecorder;
        
        /**
         * what fields to fetch when working with the tree
         *
         * @var string
         */
        private $fields;
        
        /* -------------- END VARIABLE DECLARATION --------------- */

        /**
         * tree::__construct()
         * 
         * @param string $table
         * @param object $db
         * @return void
         **/
        public function __construct(ADOConnection $db,$table='structure',$errorRecorder=null,$fields='id, url, level'){
            $this->setDB($db);
            $this->setTable($table);
            $this->setErrorRecorder($errorRecorder);
            $this->setFields($fields);
        }


        /**
         * tree::addNode()
         * 
         * adds a new node to the tree, all the data logic is
         * handled by the database inside a transaction. in case
         * the insertion fail an exception is raised and passed
         * to php
         * 
         * please note, that the table may have only ONE root node
         * defined, thus the method will fail in case one tries to
         * insert a second root node
         * 
         * @param string $value - The text value to be interted into the tree
         * @param unknown $parent - The ID of the parent node
         * @return integer - The ID of the newly inserted node
         **/
        public function addNode($value=null,$parent=null,$title=null,$type=null){
            //first some error checking
            //lets try to add the node
            try{
                //are all setting specifind?
                if(!empty($value) and !empty($parent) and !empty($title) and !empty($type)){
                    $newNodeID = (int)$this->db->getOne("SELECT add_node('".(string)$value."',".(int)$parent.",'".(string)$title."',".(int)$type.")");

                }elseif(!empty($value) and !empty($parent) and !empty($title)){ //no
                    $newNodeID = (int)$this->db->getOne("SELECT add_node('".(string)$value."',".(int)$parent.",'".(string)$title."')");
                
                //no title was specified
                }elseif(!empty($value) and !empty($parent)){
                    $newNodeID = (int)$this->db->getOne("SELECT add_node('".(string)$value."',".(int)$parent.")");

                }else{ //yes, this is the root case
                    $newNodeID = (int)$this->db->getOne("SELECT add_node()");
                }
                
                return $newNodeID;
                
            }catch(exception $e){   //something went wrong

                //
                // apparently there are only two errors that could 
                // be raised from this function: 
                //
                //   * the duplication of a unique key
                //     simply meaning that the $value field has to
                //     be redefined
                //
                //   * the defined parent node does not exist
                //     
                
                //record error
                $this->logError($e->getMessage(),$e->getCode());
                echo $e->getMessage();
				exit;
                return false;
            }
        }


        /**
         * tree::dropNode()
         *
         * Deletes a node from the tree including all its
         * children. All the data logic is with the database.
         * The procedure is carried out inside a transaction.
         * 
         * @param integer $id - ID of the node to be deleted
         * @return bool - True on success, False on failure
         **/
        public function dropNode($id){
            //ok, let's dance
            try{
                //pass the directions to the database
                $this->db->Execute("SELECT drop_node(".(int)$id.")");
                
                //all was cool, everything deleted as needed
                return true;
            }catch(exception $e){   //something failed
            
                //record error
                $this->logError($e->getMessage(),$e->getCode());
                
                return false;
            }
        }

    
        /**
         * tree::getNode()
         * 
         * Returns all node settins specified by the first parameter
         *
         * @param integer $id
         * @return array
         **/
        public function getNode($id=null){
            //let's dance
            try{
                // get the path to the node from the DB
                return $this->db->getRow("SELECT * FROM ".$this->getTable()." WHERE id = ".(int)$id);
            }catch(exception $e){ // something went wrong
                
                //record error
                $this->logError($e->getMessage(),$e->getCode());
                
                return false;
            }
        
        }


        /**
         * tree::getPath()
         * 
         * Returns the full path to any given node separated by slashes
         *
         * As with the other methods, all the data logic is with the
         * database inside a UDF, please read the UDF's description for
         * more info.
         *
         * This UDF has been optimized for speed with the aid of an
         * additional column in the table refered to as path_cache
         *
         * All the actions are carried out inside a transaction
         *
         * @param integer $id - ID of the node to which to get the path to
         * @return string - Full path to that node
         **/
        public function getPath($id){
            //let's dance
            try{
                // get the path to the node from the DB
                return $this->db->getOne("SELECT get_path(".(int)$id.")");
            }catch(exception $e){ // something went wrong
            
                //record error
                $this->logError($e->getMessage(),$e->getCode());
                
                return false;
            }
        }
        
        
        
        /**
         * Returns an array-stack of the full path to the selcted node
         * This procedure may come in handly when building stuff like
         * breadcrumb arrays while navigating the data tree
         *
         * @param integer $id
         * @param bool $returnRoot
         * @return array
         */
        public function getPathStack($id, $returnRoot = false){
            //first get position of node on tree
            $node = $this->getNode($id);
            
            if(!$returnRoot){
                $cond = 'AND parent_id IS NOT NULL';
            }else{
                $cond = null;   
            }
            
            //now try to get the stack
            try{
                return $this->db->getAll("SELECT 
                                                 ".$this->getFields()."
                                            FROM ".$this->getTable()." 
                                            WHERE lft <= ".(int)$node['lft']." 
                                            AND rgt >= ".(int)$node['rgt']." ".$cond." 
                                            ORDER BY lft ASC");
                
            }catch(exception $e){ // something went wrong
            
                //record error
                $this->logError($e->getMessage(),$e->getCode());
                
                return false;
            }
        }
        
        
        
        /**
         * Returns a list of children for the selected node, the second parameter
         * indicates wheter to return the nodeid as a child of itself.
         *
         * @param integer $id
         * @param bool $including
         * @return array or false
         */
        public function getChildren($id, $including=false, $order=null){
            
            //get the data for the current node
            /*
                Yes, I know, that this data has already been fetched while navigating to that node
                yet in order to keep things clean this step is a neccessity to make things a bit
                more portable. In case performance becomes an issue, this is a place, where
                it may be optimized
            */
            $node = $this->getNode($id);
            
            //is there such a node?
            if($node){
                //do we return the base node as well?
                if($including){
                    return $this->db->getAll("SELECT ".$this->getFields()." FROM ".$this->getTable()." WHERE lft >= ".$node['lft']." AND rgt <= " . $node['rgt']." ".($order?" ORDER BY ".$order:""));
                }else{
                    return $this->db->getAll("SELECT ".$this->getFields()." FROM ".$this->getTable()." WHERE lft > ".$node['lft']." AND rgt < " . $node['rgt']." ".($order?" ORDER BY ".$order:""));
                }
            }
            return false;
        }


        /**
         * tree::getTree()
         * 
         * Returns the whole tree in form of an array
         *
         * Note: this query makes a call to the UDF get_path() which will
         *       recalculate all the paths on the tree. This may be a costly
         *       procedure in case the whole tree is fetched at once. Thus 
         *       please be careful and use pagers
         *
         * @param integer $id - ID of the node from which to get the tree from
         * @param integer $level - max depth to go to
         * @param bool $getRoot - whether to return the roon node as well
         * @return array
         **/
        public function getTree($id=null, $level=null, $getRoot=true, $const=null, $order=null){
            try{
                $conditions = array();

                //in case the limiting factor has been set, then
                //return just a portion of the tree
                if(isset($id)){
                    //get the lft and rgt values for the node in question
                    $node = $this->getNode($id);
                    
                    //add a condition to the limiting stack
                    $conditions[] = "lft > ".$node['lft']." AND rgt < ".$node['rgt'];
                }
                
                //check for max depth
                if(isset($level)){
                    //add a condition to the limiting stack
                    $conditions[] = "level <= ".$level;
                }

                //do we need the root node?
                if(!$getRoot){
                    //add a condition to the limiting stack
                    $conditions[] = "parent_id IS NOT NULL";
                }
                
                if(isset($const)){
                    $conditions[] = $const;   
                }
                
                //form SQL
                $sql = "SELECT ".$this->getFields()." FROM ".$this->getTable();
                
                if(!empty($conditions)){
                    $sql .= " WHERE ".implode(" AND ",$conditions);
                }

                if($order){
                    $sql .= " ORDER BY ".$order." ASC";
                }else{
                    $sql .= " ORDER BY lft ASC";
                }

                return $this->db->getAll($sql);

            }catch(exception $e){ //the world has gone mad, there were errors in the query
                
                //record error
                $this->logError($e->getMessage(),$e->getCode());
                
                return false;
            }
        }


        /**
         * tree::moveNode()
         * 
         * Moves part of the tree to a new destination. In case a move
         * is not possible due to logical constraints, then an exception
         * will be thrown. The third parameter defines after which node
         * the new subtree will be inserted. This is particularly useful
         * in case the order of the nodes under some given parent is important
         *
         * As with the other methods, all the data logic is with the
         * database inside a UDF, please read the UDF's rescription for
         * more info.
         *
         * All the actions are carried out inside a transaction
         *
         * @param integer $source - ID of source node to be moved
         * @param integer $destination - ID of new parent node for the source
         * @param integer $after - ID of the node after which to insert the source
         * @return 
         **/
        public function moveNode($source,$destination,$after=null){
            //let's dance
            try{
                //see if the third parameter was set, thus change the query a little
                if(!isset($after)){
                    $this->db->Execute("SELECT move_tree(".(int)$source.",".(int)$destination.")");
                }else{
                    $this->db->Execute("SELECT move_tree(".(int)$source.",".(int)$destination.",".(int)$after.")");
                }
                
                return true;
            }catch(exception $e){ // something went wrong
            
                //record error
                $this->logError($e->getMessage(),$e->getCode());
                
                return false;
            }
        }


        /**
         * tree::traverse()
         *
         * This is a system method, which will restore the lft and rgt
         * indexes of a tree in case they have been ruined by any
         * particular event. This function is a wrapper for the _traverse()
         * function.
         * 
         * @return bool
         **/
        public function traverse(){
            //find the root node
            $rootNodeID = (int)$this->db->getOne("SELECT id FROM ".$this->getTable()." WHERE parent_id IS NULL");
            return (bool)$this->_traverse($rootNodeID);
        }

        
        /**
         * tree::_traverse()
         *
         * This is the actual traversal engine called by the wrapper defined
         * above. This is a recursive function.
         *
         *  @param integer $parent -- ID of node from which the traversal is to take place
         *  @param integer $left -- the Left index of tha node from which traversal is to take place
         * 
         * @return integer 
         **/
        protected function _traverse($parent=null,$left=1){
            // the right value of this node is the left value + 1
            $right = $left+1;

            // get all children of this node
            $result = $this->db->getCol("SELECT id FROM ".$this->getTable()." WHERE parent_id = ".$parent);
            
            // loop through the result set
            foreach($result as $entry){
                $right = $this->_traverse($entry, $right);
            }

            // we've got the left value, and now that we've processed
            // the children of this node we also know the right value
            $this->db->Execute("UPDATE ".$this->getTable()." SET lft = ".$left.", rgt = ".$right." WHERE id = ".$parent);

            // return the right value of this node + 1
            return $right+1; 
        }


        /**
         * tree::setDB()
         * 
         * @param object $db
         * @return void
         **/
        public function setDB($db){
            $this->db = $db;
        }


        /**
         * tree::setTable()
         * 
         * @param string $table
         * @return void
         **/
        public function setTable($table){
            $this->table = $table;
        }

        
        /**
         * tree::getTable()
         * 
         * @return string
         **/
        public function getTable(){
            return $this->table;
        }
        
        
        /**
         * tree::setErrorRecorder()
         * 
         * @param $errorRecorder
         * @return 
         **/
        public function setErrorRecorder($errorRecorder){
            $this->errorRecorder = $errorRecorder;
        }

        
        /**
         * tree::setFields()
         * 
         * @return string
         **/
        public function setFields($fields){
            $this->fields = $fields;
        }


        /**
         * tree::getFields()
         * 
         * @return string
         **/
        public function getFields(){
            return $this->fields;
        }


        /**
         * tree::logError()
         * 
         * @param string $text
         * @param integer $code
         * @param boolean $silent
         * @return 
         **/
        private function logError($text,$code,$silent=true){
            if(isset($this->errorRecorder)){
                $this->errorRecorder->recordError($text,$code,$silent);
            }
        }
    }
?>